#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys, re
from munkres import Munkres, print_matrix
import time
from datetime import date

class Sheep:
   """class representing a sheep (incoming student)"""
   def __init__(self,attrlist,valuelist):
      self.attr_dict = {}
      for attr,val in zip(attrlist,valuelist):
         self.attr_dict[attr] = val
   @classmethod
   def from_tuple(cls,value_tuple):
      """creates a sheep from an (already interpreted) tuple"""
      mail,langs,country,arrival_date,faculty,gender = value_tuple
      new_sheep = cls([],[])
      new_sheep.mail = mail
      new_sheep.langs = langs.split(",")
      new_sheep.country = country
      new_sheep.arrival_date = interpret_date(arrival_date)
      new_sheep.faculty = faculty
      new_sheep.gender = gender
      return new_sheep
   def attr(self,attrname):
      return self.attr_dict[attrname]
   def get_basic_attributes(self):
      return ["mail","country","arrival_date","faculty","gender"]
   def interpret_attrs(self,conf_tuples):
      """interprets sheep attributes given interpretation (normalization) hash"""
      # computing attributes (columns) corresponding to basic attributes
      basic_attrs = set(self.get_basic_attributes())
      present_attrs = set(self.attr_dict)
      basic_attrs2columns = {}
      for attr in basic_attrs:
         basic_attrs2columns[attr] = set([])
      for column_title,basic_attr in conf_tuples:
         if basic_attr not in basic_attrs:
            print >> sys.stderr, "Unknown basic attribute \"" + basic_attr + "\"" +\
                                 "in sheep interpretation configuration."
            sys.exit(1)
         if column_title in present_attrs:
            basic_attrs2columns[basic_attr].add(column_title)
      # checking uniqueness
      for basic_attr in basic_attrs:
         if   len(basic_attrs2columns[basic_attr]) == 0:
            print >> sys.stderr, "Sheep interpretation: no attribute found for " + basic_attr + "!"
            sys.exit(1)
         elif len(basic_attrs2columns[basic_attr]) > 1:
            print >> sys.stderr, "Sheep interpretation: ambiguous column choice for " + basic_attr + ": " +\
                                 basic_attrs2columns[basic_attr]
      # interpreting attributes
      self.mail = self.attr_dict[ basic_attrs2columns["mail"].pop() ]
      #self.langs = self.attr_dict[ basic_attrs2columns["langs"].pop() ].split(",")
      self.country = self.attr_dict[ basic_attrs2columns["country"].pop() ]
      self.arrival_date = interpret_date(self.attr_dict[ basic_attrs2columns["arrival_date"].pop() ])
      self.faculty = self.attr_dict[ basic_attrs2columns["faculty"].pop() ]
      self.gender = self.attr_dict[ basic_attrs2columns["gender"].pop() ]
      self.langs = Sheep_lang_interpreter.interpret_langs(self)

class Sheep_lang_interpreter:
   @classmethod
   def direct_lang_columns2en_names(cls):
      return {'English':'English', 'Czech':'Czech'}
   @classmethod
   def other_lang_columns(cls):
      return [('Other language 1','Other level 1'),\
              ('Other language 2','Other level 2'),\
              ('Other language 3','Other level 3')]
   @classmethod
   def hash2list(cls,lang_hash):
      lang_list = []
      for lang in lang_hash:
         if lang == 'Czech':
            if lang_hash[lang] not in set(['poor','']):
               lang_list.append(lang)
         elif lang_hash[lang] not in set(['poor']):
            lang_list.append(lang)
      return lang_list
   @classmethod
   def interpret_langs(cls,sheep):
      direct_hash = cls.direct_lang_columns2en_names()
      other_list = cls.other_lang_columns()
      lang_hash = {}
      # obtain lang:level (for direct lang columns)
      for direct_lang in direct_hash:
         direct_lang_level = sheep.attr(direct_lang)
         if direct_lang_level:
            lang_hash[direct_hash[direct_lang].strip()] = direct_lang_level.strip()
      # obtain lang:level (for other lang columns)
      for other_tuple in other_list:
         other_name_column,other_level_column = other_tuple
         other_name = sheep.attr(other_name_column)
         other_level = sheep.attr(other_level_column)
         if other_name:
            lang_hash[other_name.strip()] = other_level.strip()
      #print lang_hash, len(set(lang_hash.values()))
      if len(set(lang_hash.values())) == 0:
         # no values => langs unknown
         return ["unknown"]
      return cls.hash2list(lang_hash)
      

class Shepherd:
   """class representing a shepherd (tutor/buddy)"""
   def __init__(self,attrlist,valuelist):
      self.attr_dict = {}
      for attr,val in zip(attrlist,valuelist):
         self.attr_dict[attr] = val
   @classmethod
   def from_tuple(cls,value_tuple):
      """creates a sheep from an (already interpreted) tuple"""
      mail,langs,country_prefs,ava_date,faculty,gender,capacity = value_tuple
      new_shepherd = cls([],[])
      new_shepherd.mail = mail
      new_shepherd.langs = langs.split(",")
      new_shepherd.country_prefs = country_prefs.split(",")
      new_shepherd.ava_date = interpret_date(ava_date)
      new_shepherd.faculty = faculty
      new_shepherd.gender = gender
      new_shepherd.capacity = int(capacity)
      return new_shepherd
   def attr(self,attrname):
      return self.attr_dict[attrname]
   def get_basic_attributes(self):
      return ["mail","langs","country_prefs","ava_date","faculty","gender","capacity"]
   def interpret_attrs(self,conf_tuples):
      """interprets shepherd attributes given interpretation (normalization) hash"""
      # computing attributes (columns) corresponding to basic attributes
      basic_attrs = set(self.get_basic_attributes())
      present_attrs = set(self.attr_dict)
      basic_attrs2columns = {}
      for attr in basic_attrs:
         basic_attrs2columns[attr] = set([])
      for column_title,basic_attr in conf_tuples:
         if basic_attr not in basic_attrs:
            print >> sys.stderr, "Unknown basic attribute \"" + basic_attr + "\"" +\
                                 "in sheep interpretation configuration."
            sys.exit(1)
         if column_title in present_attrs:
            basic_attrs2columns[basic_attr].add(column_title)
      # checking uniqueness
      for basic_attr in basic_attrs:
         if   len(basic_attrs2columns[basic_attr]) == 0:
            print >> sys.stderr, "Shepherd interpretation: no attribute found for " + basic_attr + "!"
            sys.exit(1)
         elif len(basic_attrs2columns[basic_attr]) > 1:
            print >> sys.stderr, "Sheep interpretation: ambiguous column choice for " + basic_attr + ": " +\
                                 basic_attrs2columns[basic_attr]
      # interpreting attributes
      self.mail = self.attr_dict[ basic_attrs2columns["mail"].pop() ]
      self.langs = self.attr_dict[ basic_attrs2columns["langs"].pop() ].split(", ")
      self.country_prefs = self.attr_dict[ basic_attrs2columns["country_prefs"].pop() ].split(", ")
      self.ava_date = interpret_date(self.attr_dict[ basic_attrs2columns["ava_date"].pop() ])
      self.faculty = self.attr_dict[ basic_attrs2columns["faculty"].pop() ]
      self.gender = self.attr_dict[ basic_attrs2columns["gender"].pop() ]
      self.capacity = int(self.attr_dict[ basic_attrs2columns["capacity"].pop() ])

# INTERPRETING COLUMNS

def interpret_attrs(x_list, conf_filename):
   """interprets basic sheep/shepherd attributes using the conf file"""
   # read the interpretation hash
   interpretation_tuples = conf_file2tuples(conf_filename)
   # interpret sheep/shepherds attributes
   for x in x_list:
      x.interpret_attrs(interpretation_tuples)

# INTERPRETING DATE VALUES

def interpret_date(date_string):
   """returns a date object"""
   if   re.match(r"0000-00-00",date_string):
      return "unknown"
   elif date_string == "":
      return "unknown"
   elif re.match(r"\d\d\d\d[-]\d\d[-]\d\d",date_string):
      # 2012-06-29
      year,month,day = re.split(r"[-]",date_string)
      return date(int(year), int(month), int(day))
   elif re.match(r"\d\d[/]\d\d[/]\d\d\d\d",date_string):
      # 06/22/1987
      month,day,year = re.split(r"[/]",date_string)
      return date(int(year), int(month), int(day))
   print >> sys.stderr,"Warning: Unable to interpret date \"" + date_string + "\""
   sys.exit(1)

def interpreted_date_as_str(d):
   """returns a string representation of the given date object"""
   if d == "unknown":
      return "unknown"
   else:
      return d.isoformat()

# NORMALIZING/TRANSFORMING VALUES

def normfile2hash(filename):
   """returns a hash determining normalization of values"""
   f = open(filename, 'r')
   normhash = {}
   for line in f.readlines():
      line = line.strip()
      if re.match(r".*->.*",line):
         before,after = re.split(r"->",line)
         normhash[before] = after
   return normhash

def normalize(norm_hash,value):
   """given a normalization hash, normalizes the given value"""
   if value in norm_hash.keys():
      return norm_hash[value]
   print >> sys.stderr,"Warning: Unable to normalize \"" + value + "\""
   sys.exit(1)

def conf_file2tuples(filename):
   f = open(filename, 'r')
   transform_tuples = []
   for line in f.readlines():
      line = line.strip()
      if re.match(r".*->.*",line):
         before,after = re.split(r"->",line)
         transform_tuples.append( (before,after) )
   return transform_tuples

# PAIRING

def couple_sheep(sheep_key_list,shepherd_key_list,sheep_hash,shepherd_hash,eval_method):
   """Matches sheep with sheperds

   Arguments:
   sheep_key_list -- list of keys representing sheep to match
   shepherd_key_list -- list of keys representing shepherds to match
   sheep_hash -- dictionary sheep_key -> sheep object
   shepherd_hash -- shepherd_key -> shepherd object
   eval method -- method returning weight of a sheep-shepherd combination
   """
   # create a coupling matrix (and flip weight ~> cost)
   max_val = 20
   matrix = []
   for sheep in sheep_key_list:
      row = []
      for shepherd in shepherd_key_list:
         row.append(max_val - eval_method(sheep_hash[sheep],shepherd_hash[shepherd]))
      matrix.append(row)
   # make rectangular
   if len(sheep_key_list) < len(shepherd_key_list):
      # add rows
      for i in range(1,len(shepherd_key_list)-len(sheep_key_list)+1):
         matrix.append([max_val]*len(shepherd_key_list))
   elif len(sheep_key_list) > len(shepherd_key_list):
      # add columns
      for row in matrix:
         row.extend([max_val]*(len(sheep_key_list) - len(shepherd_key_list)))
   # prepare
   couples = []
   used_sheep = []
   used_shepherds = []
   free_sheep = []
   free_shepherds = []
   if len(sheep_key_list) > 0 and len(shepherd_key_list) > 0:
      # calculate the pairing
      m = Munkres()
      munkres_result = m.compute(matrix)
      # interpret result
      for x,y in munkres_result:
         if not matrix[x][y] == max_val:
            #print sheep_key_list[x],shepherd_key_list[y],str(max_val-matrix[x][y])
            couples.append((sheep_key_list[x],shepherd_key_list[y]))
            used_sheep.append(sheep_key_list[x])
            used_shepherds.append(shepherd_key_list[y])
   # compile result
   free_sheep = list(set(sheep_key_list)-set(used_sheep))
   free_shepherds = list(set(shepherd_key_list)-set(used_shepherds))
   return couples,free_sheep,free_shepherds

def update_ava_shepherds(ava_shepherds,all_unused_shepherds,shepherd_hash):
   """Updates the list of available shepherds
   (useful at the end of an assignment round)
   Detracts 1 from each unused shepherd's capacity
   and removes the shepherd from ava_shepherds when his capacity reaches 0
   """
   unused_shepherd_set = set(all_unused_shepherds)
   to_delete=set([])
   for shepherd in ava_shepherds:
      if not shepherd in unused_shepherd_set:
         shepherd_hash[shepherd].capacity = shepherd_hash[shepherd].capacity - 1
         if shepherd_hash[shepherd].capacity == 0:
            to_delete.add(shepherd)
   for shepherd in to_delete:
      ava_shepherds.remove(shepherd)
   return

